package auctionClient;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.util.Scanner;
import java.util.Vector;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.bouncycastle.openssl.PEMReader;
import org.bouncycastle.openssl.PasswordFinder;
import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.util.encoders.Hex;

public class Client {
	private static int tcpPort;
	private static InetAddress host;
	private static boolean loggedIn=false;
	private static String sessionUser;
	private static TcpConnection connection;
	private static int clienttcpPort;
	private static String serverPublicKeyFile;
	private static String clientsKeyDir;
	private static String userName="";
	private static SecretKey key = null;
	private static IvParameterSpec ivSpec = null;
	private static Key sharedKey=null;
	private static boolean serverIsOnline=false;
	private static Vector<onlineUser> onlineUsers= new Vector<onlineUser>();
	private static BufferedReader reader;
			
	public static void main(String [] args) throws UnknownHostException {
		System.out.println("Auction client is running");		
		/*
		 * Die Variablen Belegung fuer die tcpPort, udpPort, host Variablen.
		 */
		
		host=java.net.InetAddress.getByName(args[0]);
		tcpPort=Integer.parseInt(args[1]);
		clienttcpPort=Integer.parseInt(args[2]);
		serverPublicKeyFile=args[3];
		clientsKeyDir=args[4];
		sharedKey=defaultKey();
		String userLine="";
		String command="";
		// Timestamp listener will be started!
		ClientTimeStamp clientTimestamp= new ClientTimeStamp(clienttcpPort, clientsKeyDir);
		Thread timestampThread=new Thread(clientTimestamp);
		timestampThread.start();
		
		reader= new BufferedReader(new InputStreamReader(System.in));
		Scanner stringReader;
		
		/*
		 * die Verbindung zum Server
		 */
		
		connection=new TcpConnection(tcpPort, host);
		try {
			connection.connect();
			serverIsOnline=true;
		} catch (IOException e1) {
			System.out.println("No Online server is found!");
		}
		/*
		 * Befehle werden eingelesen
		 */
		while(true){
			checkIsServerOnline();
			if(!serverIsOnline){
				doLogout();
			}

			try {
				
				if(loggedIn==true){
					System.out.print(userName+">:");
				}
				userLine=reader.readLine();		
			} catch (IOException e) {
				e.printStackTrace();
				
			}
			stringReader=new Scanner(userLine);
			command=stringReader.next();
			
			if(command.equals("!login")){
				if(loggedIn==true){
					System.out.println("You are already logged in! You can't log in more than once!");
				}else{
					try{
						userName=stringReader.next();				
					}catch(java.util.NoSuchElementException e){
						System.out.println("please enter a user name!");
						continue;
					}
					doLogin(userName);
				}
			}	
			else if(command.equals("!list")){	
				checkIsServerOnline();
				if(serverIsOnline){
					doList(true);
				}else{
					System.out.println("Server is offline");
					if(loggedIn){
						doLogout();
					}
				}
			}
			
			else if(command.equals("!end")){
				try{
					connection.close();
 				}catch(Exception e ){
					System.out.println("ERROR at closing the connection(Client :109)!");
				}
				try {
					reader.close();
				} catch (IOException e) {
					System.out.println("ERROR at closing the Buffered reader(Client :114)!");
				}
				
				break;
			}
			else if(command.equals("!login")==false&&command.equals("!list")==false&&
					command.equals("!end")==false&&command.equals("!logout")==false&&
					command.equals("!confirm")==false&&command.equals("!groupbid")==false&&
					command.equals("!create")==false&&command.equals("!bid")==false){
				System.out.println("Unknown Command or Syntax Error please make sure you provide the system \n" +
									"with known commands with the right Syntax: !<command> <optional parameter1> <optional parameter2>");
			}
			else{
				
				if(loggedIn==false){
					System.out.println("You need to log in to perform this action");
				}else{

					if(command.equals("!logout")){
						doLogout();
					}
			
					else if(command.equals("!create")){
						checkIsServerOnline();
						if(serverIsOnline){
							try{
								doCreate(stringReader.nextInt(),stringReader.next());
							}catch(Exception e){
								System.out.println("Illegal Usage! \nThe command !create has to be followed by <int> and <String>");
							}
						}else{
							System.out.println("Server is offline");
							doLogout();
						}
						

					}
					
					else if(command.equals("!bid")){
						try{
							doBid(stringReader.nextInt(),stringReader.nextInt());
						}catch(Exception e){
							e.printStackTrace();
							System.out.println("Illegal Usage! \nThe command !bid has to be followed by <int 1> <int 2>");
						}
						
					}
					else if(command.equals("!groupbid")){
						try{
							doGroupBid(stringReader.nextInt(),stringReader.nextInt());
						}catch(Exception e){
							e.printStackTrace();
							System.out.println("Illegal Usage! \nThe command !groupbid has to be followed by <int 1> <int 2>");
						}
						
					}
					else if(command.equals("!confirm")){
						try{
							doConfirm(stringReader.nextInt(), stringReader.nextInt(), stringReader.next());
						}catch(Exception e){
							e.printStackTrace();
							System.out.println("Illegal Usage! \nThe command !confirm has to be followed by <int 1> <int 2> <string>");
						}
						
					}
				}
			}
		}
	}

	private static void doLogin(String userName){
		SecureRandom secureRandom = new SecureRandom(); 
		byte[] challengeNumber = new byte[32]; 
		secureRandom.nextBytes(challengeNumber);
		byte[] challengeNumberBase64 = Base64.encode(challengeNumber);
		
		byte[] message= ("!login "+userName+" "+clienttcpPort+" "+(new String(challengeNumberBase64))).getBytes();
		//RSA cipher
		try {
			@SuppressWarnings("resource")
			PEMReader inServerPublicKey = new PEMReader(new FileReader(serverPublicKeyFile));
			PublicKey publicKey = (PublicKey) inServerPublicKey.readObject();
			Cipher rsaCipher = Cipher.getInstance("RSA/NONE/OAEPWithSHA256AndMGF1Padding");
			rsaCipher.init(Cipher.ENCRYPT_MODE, publicKey);
			byte[] cipherMessage = rsaCipher.doFinal(message);
			
			connection.appendMessage(cipherMessage);

			byte[] responseCipher=connection.getMessage();
 
			@SuppressWarnings("resource")
			PEMReader inClientPrivateKey = new PEMReader(new FileReader(clientsKeyDir + userName + ".pem"), new PasswordFinder() {
				@Override
				public char[] getPassword() {
					char[] res = "".toCharArray();
					System.out.println("Enter pass phrase:");
					try {
						res = reader.readLine().toCharArray();
					} catch (IOException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					return res;
				}
			});
			KeyPair keyPair = (KeyPair) inClientPrivateKey.readObject();
			PrivateKey privateKey = keyPair.getPrivate();
			rsaCipher = Cipher.getInstance("RSA/NONE/OAEPWithSHA256AndMGF1Padding");
			rsaCipher.init(Cipher.DECRYPT_MODE, privateKey);
			String responseClear = new String(rsaCipher.doFinal(responseCipher));

			if(responseClear.equals("already logged in")){
				System.out.println(userName +" already logged in System "+ "!");
			}
			else {
				Scanner reader= new Scanner(responseClear);
				if (reader.next().equals("!ok"))
				{
					String challengeNumber_recieved = reader.next();
					if (challengeNumber_recieved.equals(new String(challengeNumberBase64)))
					{
						String serverChallenge = reader.next();
						
						byte[] keyBytes = Base64.decode(reader.next());
						key = new SecretKeySpec(keyBytes, "AES");
						byte[] ivSpecBytes = Base64.decode(reader.next());
						ivSpec = new IvParameterSpec(ivSpecBytes);
						
						loggedIn=true;
						sessionUser=userName;
						sharedKey= readKey();

						System.out.println("Succesfully logged in as "+ sessionUser +" !");
						
						Cipher aesCipher = Cipher.getInstance("AES/CTR/NoPadding");
						aesCipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
						cipherMessage = aesCipher.doFinal(serverChallenge.getBytes());
						connection.appendMessage(cipherMessage);
						doGetClientsList();
					}
				}
			}
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (NoSuchAlgorithmException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (NoSuchPaddingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InvalidKeyException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalBlockSizeException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (BadPaddingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InvalidAlgorithmParameterException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	}
	
	private static void doList(boolean first){
		boolean valid=false;
		connection.appendMessage("!list".getBytes());
		String response = new String(connection.getMessage());
		HMACVerifier hashVerifier=new HMACVerifier(response, sharedKey);
			valid=hashVerifier.verify();

		if(valid){
			response=response.substring(0, response.indexOf("[HMAC]"));
			int iteration = Integer.valueOf(response.substring(0, response.indexOf("@")));
			String command = response.substring(response.indexOf("@") + 1, response.length());
			if(iteration==0){
				System.out.println("No Auctions listed on server");
			}
			else{
				System.out.println(command);
			}
		}
		else if(first){
			System.out.println("Corrupted message has been received from Server\n" +
					"a second attempt for a new output will be sent to Server... ");
			doList(false);
		}
		else{
			System.out.println("Second attempt is also corrupted " +
					"Channel is being manuplated! ");
			first=true;
		}
	}
	
	private static void doLogout(){
		checkIsServerOnline();
		if(serverIsOnline){
			connection.appendMessage(getEncryptedMessageByASE(("!logout "+sessionUser).getBytes()));
			
			byte[] response = getDecryptedMessageByASE(connection.getMessage());
			
			if(response.equals("logged out")){
				loggedIn=false;
				System.out.println("Succesfully logged out as "+ sessionUser +" !");
			}else if(response.equals("mismatch")){
				System.out.println("ERROR: Session User Mismatch");
			}
		}else{
			loggedIn=false;
		}
	}
	
	private static void doCreate(int duration, String description){
		connection.appendMessage(getEncryptedMessageByASE(("!create "+duration+" "+description).getBytes()));
		byte[] serverMessage = getDecryptedMessageByASE(connection.getMessage());

		Scanner reader= new Scanner(new String(serverMessage));
		
		if(reader.next().equals("created")){
			System.out.println("An auction '"+description+"' with id: "+reader.nextInt()+" has been created and will end on: "+reader.nextLine());
			reader.close();
		}else{
			System.out.println("No auction '"+description+"' has been created try again");
			reader.close();
		}
	}
	
	private static void doGroupBid(int auctionId, int amount){
		checkIsServerOnline();
		if(serverIsOnline){
			connection.appendMessage(getEncryptedMessageByASE(("!groupbid "+auctionId+" "+amount).getBytes()));
			String response = new String(getDecryptedMessageByASE(connection.getMessage()));
			
			System.out.println(response);
		}else{
			doLogout();
		}
	}
	
	private static void doConfirm(int auctionID, int amount, String bidder){
		checkIsServerOnline();
		if(serverIsOnline){
			connection.appendMessage(getEncryptedMessageByASE(("!confirm "+auctionID+" "+amount+" "+bidder).getBytes()));
			String response = new String(getDecryptedMessageByASE(connection.getMessage()));
			
			System.out.println(response);
		}else{
			doLogout();
		}
	}

	private static void doBid(int auctionId, int amount){
		checkIsServerOnline();
		if(serverIsOnline){
			connection.appendMessage(getEncryptedMessageByASE(("!bid "+auctionId+" "+amount).getBytes()));
			String response = new String(getDecryptedMessageByASE(connection.getMessage()));
			
			String command = response.substring(0, response.indexOf("@"));
			String message = response.substring(response.indexOf("@") + 1, response.length());
			
			if(command.equals("bid accepted")){
				System.out.println("You succesfully bid with "+amount+" with id "+auctionId+" on "+message);
			}else if(command.equals("bid denied")){
				System.out.println("You unsuccesfully bid with "+amount+" with id "+auctionId+" on "+message);
			}else if(command.equals("not found")){
				System.out.println("Auction with id "+auctionId+" isn't active.");
			}
		}else{
			doSignedBid(auctionId,amount);
			doLogout();
		}
	}
	
	private static void doSignedBid(int auctionID, int price) {
		System.out.println("Server is Offline, your bid needs to be signed by 2 other online users please wait...");
		
		TcpConnection connUser1 = null;	
		TcpConnection connUser2 = null;
	
		int userNr=0;
		int connNr=0;
		String user1="";
		String user2="";
		String timestamp1="";
		String timestamp2="";
		String signature1="";
		String signature2="";

		while(connNr<2&&userNr<onlineUsers.size()){
			if(onlineUsers.get(userNr).getName().equals(getUserName())){
				userNr++;
			}else if(connNr==0){
				connUser1 =new TcpConnection(onlineUsers.get(userNr).getTcpPort(),host );
				try {
					connUser1.connect();
					user1=onlineUsers.get(userNr).getName();
					connNr++;
					userNr++;
					
				} catch (IOException e) {
					userNr++;
				}
			}else if(connNr==1){
				connUser2 =new TcpConnection(onlineUsers.get(userNr).getTcpPort(),host );
				try {
					connUser2.connect();
					user2=onlineUsers.get(userNr).getName();
					connNr++;
					userNr++;
				} catch (IOException e) {
					userNr++;
				}
			}
		}
		
		if(connUser1==null||connUser2==null){
			System.out.println("Too less online users for signedBidding try bidding later");
		}else{
				
			if(connUser1!=null){
				connUser1.appendMessage(("!getTimestamp "+auctionID+" "+price).getBytes());
				String response1=new String(connUser1.getMessage());
				Scanner temp=new Scanner(response1);
				temp.next();
				temp.next();
				temp.next();
				timestamp1=temp.next();
				signature1=temp.next();
			}
			
			if(connUser2!=null){
				connUser2.appendMessage(("!getTimestamp "+auctionID+" "+price).getBytes());
				String response2=new String(connUser2.getMessage());
				Scanner temp=new Scanner(response2);
				temp.next();
				temp.next();
				temp.next();
				timestamp2=temp.next();
				signature2=temp.next();
				
			}
			
			String line="!signedBid "+auctionID+" "+price+" "+user1+":"+timestamp1+":"+signature1+" "+user2+":"+timestamp2+":"+signature2;
			SignedBidSender sender= new SignedBidSender(connection, line, price, auctionID);
			Thread senderTimer=new Thread(sender);
			senderTimer.start();
			
			

		}
	}
	
	private static void doGetClientsList(){
		onlineUsers=new Vector<onlineUser>();

		connection.appendMessage(getEncryptedMessageByASE(("!getClientList").getBytes()));
		String response=new String(getDecryptedMessageByASE(connection.getMessage()));
		response = response.substring(response.indexOf("@") + 1, response.length());
		Scanner stringReader=new Scanner(response);
		
		while(stringReader.hasNext()){
			onlineUsers.add(new onlineUser(stringReader.nextInt(), stringReader.next()));
		}
		stringReader.close();
	}
	
	private static byte[] getEncryptedMessageByASE(byte[] message){
		byte[] responseMessage = "".getBytes();
		try {
			Cipher aesCipher = Cipher.getInstance("AES/CTR/NoPadding");
			aesCipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
			responseMessage = aesCipher.doFinal(message);
		} catch (NoSuchAlgorithmException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		} catch (NoSuchPaddingException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		} catch (InvalidKeyException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InvalidAlgorithmParameterException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalBlockSizeException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (BadPaddingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return responseMessage;
	}	
	
	private static byte[] getDecryptedMessageByASE(byte[] message){
		byte[] responseMessage = "".getBytes();
		try {
			Cipher aesCipher = Cipher.getInstance("AES/CTR/NoPadding");
			aesCipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
			responseMessage = aesCipher.doFinal(message);
		} catch (NoSuchAlgorithmException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		} catch (NoSuchPaddingException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		} catch (InvalidKeyException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InvalidAlgorithmParameterException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalBlockSizeException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (BadPaddingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return responseMessage;
	}
	
	private static Key readKey(){

		byte[] keyBytes = new byte[1024];
		//TODO insert path!
		String pathToSecretKey =clientsKeyDir+getUserName()+".key";
		if(getUserName().equals("")){
			return null;
		}else{
			FileInputStream fis = null;
			try {
				fis = new FileInputStream(pathToSecretKey);
			} catch (FileNotFoundException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			try {
				fis.read(keyBytes);
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			try {
				fis.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			byte[] input = Hex.decode(keyBytes);
			// make sure to use the right ALGORITHM for what you want to do 
			// (see text) 
			
			return new SecretKeySpec(input,"AES");
		}
		
		
	}

	private static Key defaultKey(){
		byte[] keyBytes = new byte[1024];
		//TODO insert path!
		String pathToSecretKey =clientsKeyDir+"default.key";
		FileInputStream fis = null;
		try {
			fis = new FileInputStream(pathToSecretKey);
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		try {
			fis.read(keyBytes);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		try {
			fis.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		byte[] input = Hex.decode(keyBytes);
		// make sure to use the right ALGORITHM for what you want to do 
		// (see text) 
		
		return new SecretKeySpec(input,"AES");
		
		
	}
	
	private static void checkIsServerOnline(){
		if(serverIsOnline){
			connection.appendMessage("!isAlive?".getBytes());
			try{
				String message=new String(connection.getMessage());
				if(message.equals("I/O Read Error at client Side at the Socket")){
					serverIsOnline=false;
					connection.close();
				}else serverIsOnline=true;
			}catch(java.lang.NullPointerException e){
				serverIsOnline=false;
				connection.close();
			}
		}else{
			connection=new TcpConnection(tcpPort, host);
			try {
				connection.connect();
				serverIsOnline=true;
			} catch (IOException e1) {
			}
		}
	}
	
	public static String getUserName() {
		return userName;
	}

	
	
	
}